package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONNull;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;

/*
* 根据商铺id查询商户信息
* 演示了：1.缓存穿透问题的解决办法
*       2.使用互斥锁的方式解决缓存击穿问题
*       3.使用设置逻辑过期时间的方式解决缓存击穿问题
* */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 根据id查询商铺信息
     * @param id 商铺id
     * @return 商铺详情数据
     * 现在我们要把查询到的信息添加redis缓存
     */
    @Override
    public Result queryShopById(Long id) {
        /*1.查询商铺信息解决缓存穿透问题*/
//        Shop shop = queryWithPassThrough(id);

        /*2.查询商铺信息解决缓存击穿问题(互斥锁版本)(关键key解决缓存击穿问题)*/
//        Shop shop = queryWithMutex(id);
//
        /*3.查询商铺信息解决缓存击穿问题(逻辑过期)(关键key解决缓存击穿问题)*/
        Shop shop = queryWithLogicalExpire(id);

//        if(shop==null){
//            return Result.fail("店铺不存在！");
//        }
        return Result.ok(shop);
//        return Result.ok();

    }

    /*解决缓存穿透问题*/
    public Shop queryWithPassThrough(Long id){
        /*1.根据id构造redis的key,根据key去查询商户缓存*/
        String redisKey=RedisConstants.CACHE_SHOP_KEY+id;
        String redisShop = stringRedisTemplate.opsForValue().get(redisKey);
        /*2.如果没有查询到就去查询数据库*/
        if(!StringUtils.isEmpty(redisShop)){/*isEmpty()只是判断了不为null*/
            Shop shop = JSON.parseObject(redisShop, Shop.class);
//            Shop shop = JSONUtil.toBean(redisShop, Shop.class);
            return shop;
        }
        /*运行到这里redisShop可能为“”或则null*/
        if(redisShop!=null){
            return null;
        }
        /*查询数据库*/
        Shop shop = baseMapper.selectById(id);
        if(!StringUtils.isEmpty(shop)){
            String redisShopJson = JSON.toJSONString(shop);
//            String redisShopJson = JSONUtil.toJsonStr(shop);
            /*  如果数据库里面有就进行缓存重建*/
            stringRedisTemplate.opsForValue().set(redisKey,redisShopJson,RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES);
            return shop;
        }
        /*3.如果数据库里面也没有，就设置一个空对象到redis里面*/
        stringRedisTemplate.opsForValue().set(redisKey, "",RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES);
        return shop;
    }


    /*查询商户缓存解决缓存击穿问题---互斥锁的方式*/
    public Shop queryWithMutex(Long id) {
        /*1.根据id构造redis的key,根据key去查询商户缓存*/
        String redisKey=RedisConstants.CACHE_SHOP_KEY+id;
        String redisShop = stringRedisTemplate.opsForValue().get(redisKey);
        /*2.如果没有查询到就去查询数据库*/
        if(!StringUtils.isEmpty(redisShop)){/*isEmpty()只是判断了不为null*/
            Shop shop = JSON.parseObject(redisShop, Shop.class);
//            Shop shop = JSONUtil.toBean(redisShop, Shop.class);
            return shop;
        }
        /*运行到这里redisShop可能为“”或则null*/
        if(redisShop!=null){
            return null;
        }
        String lockKey=RedisConstants.LOCK_SHOP_KEY+id;
        Shop shop=null;
        boolean lockflag = lock(lockKey);
        if(lockflag){
            try {
                /*获取锁成功*/
                /*查询数据库*/
                shop = baseMapper.selectById(id);
                /*为了测试，设置休眠时间*/
                Thread.sleep(100);
                if(!StringUtils.isEmpty(shop)){
                    String redisShopJson = JSON.toJSONString(shop);
    //            String redisShopJson = JSONUtil.toJsonStr(shop);
                    /*  如果数据库里面有就进行缓存重建*/
                    stringRedisTemplate.opsForValue().set(redisKey,redisShopJson,RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES);
                    return shop;
                }
                /*3.如果数据库里面也没有，就设置一个空对象到redis里面*/
                stringRedisTemplate.opsForValue().set(redisKey, "",RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                unlock(lockKey);
            }
        }else {
            try {
                /*获取失败就重试*/
                Thread.sleep(100);
                return queryWithMutex(id);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return shop;
    }

    private static final ExecutorService CHCHE_REBUILD_EXECUTOR=Executors.newFixedThreadPool(10);


    /*查询商户缓存解决缓存击穿问题---设置逻辑过期时间*/
    /*使用逻辑过期的时候一定要进行缓存预热，确保redis缓存里面有数据，因为提前进行了缓存预热，所以就不需要考虑出现缓存穿透问题*/
    public Shop queryWithLogicalExpire(Long id){
        String redisKey=RedisConstants.CACHE_SHOP_KEY+id;
        String redisDataJson = stringRedisTemplate.opsForValue().get(redisKey);
        if(StringUtils.isEmpty(redisDataJson)){
            /*说明没有提前缓存预热*/
            return null;
        }
        RedisData redisData = JSON.parseObject(redisDataJson, RedisData.class);
        Object data = redisData.getData();
        Shop shop = JSON.parseObject(JSON.toJSONString(data), Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        boolean before = LocalDateTime.now().isBefore(expireTime);
        if(LocalDateTime.now().isBefore(expireTime)){
            /*说明redis里面的数据没有过期*/
            return shop;
        }
        /*说明数据过期了，需要进行缓存重建*/
        String lockKey=RedisConstants.LOCK_SHOP_KEY+id;
        boolean lock = lock(lockKey);
        if(lock){
            /*创建一个新的线程去进行缓存重建*/
            CHCHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    shopCache(id, 60L);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    /*缓存重建完之后需要释放锁*/
                    unlock(lockKey);
                }
            });
        }
        return shop;/*其他线程没有获得锁也需要返回脏数据给前台*/
    }

    /*缓存预热*/
    public void shopCache(Long shopId,Long expireTime){
        String redisKey=RedisConstants.CACHE_SHOP_KEY+shopId;
        Shop shop = baseMapper.selectById(shopId);
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        LocalDateTime expireTimeEnd = LocalDateTime.now().plusSeconds(expireTime);
        redisData.setExpireTime(expireTimeEnd);
        String redisValue = JSON.toJSONString(redisData);
        stringRedisTemplate.opsForValue().set(redisKey,redisValue);
    }


    /*使用Redis的setnx命令来构造锁*/
    private boolean lock(String key){
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        System.out.println(aBoolean);
        boolean flag=aBoolean;
        return flag;
    }

    /*解锁*/
    private void unlock(String key){
        Boolean aBoolean = stringRedisTemplate.delete(key);
    }

    /**
     * 更新商铺信息
     * @param shop 商铺数据
     * @return 无
     */
    @Override
    @Transactional
    public Result updateShopById(Shop shop) {
        if(shop.getId()==null){
            return Result.fail("店铺id不能为空");
        }
        String key=RedisConstants.CACHE_SHOP_KEY+shop.getId();
        /*1.更新数据库*/
        int res = baseMapper.updateById(shop);
        if(res>0){
            /*2.根据shop里面的id删除缓存*/
            /*在分布式系统中删除缓存，保持数据的一致性需要借助TTC来保证数据的一致性*/
            Boolean delete = stringRedisTemplate.delete(key);
        }
        return Result.ok();
    }



//    public void test() throws InterruptedException {
//        saveShop2Redis(1L,2000L);
//    }




}
